#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <pthread.h>
#include <math.h>
#include <thrift/concurrency/ThreadManager.h>
#include <thrift/concurrency/PosixThreadFactory.h>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/server/TThreadPoolServer.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TTransportUtils.h>
#include <pthread.h>

#include "node.h"
#include "definitions.h"
#include "NodeService.h"
#include "NodeService_server.h"
#include "NodeService_client.h"

using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;

using boost::shared_ptr;

using namespace mp2;

void* startServer(void * arg);
void* stabilizeThread(void * arg);
void* fixThread(void * arg);

int gi_stabilize_time;
int gi_fix_time;
NodeServiceHandler * g_ns;
bool server_setup = false;

/**
 * Creates our node app by launching our server,
 * and, if necessary, connects to the introducer.
 * @param argOp Options structure parsed from command line arguments
 */
NodeApp::NodeApp(nodeOptions argOp)
{
	bits = argOp.m;
	thisnode.id = argOp.id;
	thisnode.port = argOp.port;
	gi_stabilize_time = argOp.stabilizeInterval;
	gi_fix_time = argOp.fixInterval;
	//Spawn our server thread
	pthread_create(&server_thread, NULL, startServer, (void*)&argOp);
	//Join the network
	if(thisnode.id!=0)
	{
		while(!server_setup);
		sleep(3);
		NodeClient * introducer;
		NodeInfo info;
		info.port = argOp.introducerPort;
		info.id = 0;
		introducer = new NodeClient(info);
		introducer->client->addNode(thisnode);
		delete introducer;
	}
	//Spawn our timer threads
	pthread_create(&stabilize_thread, NULL, stabilizeThread, NULL);
	pthread_create(&fix_thread, NULL, fixThread, NULL);
	//Wait for the threads to join (they don't)
	pthread_join(server_thread, NULL);
	pthread_join(stabilize_thread, NULL);
	pthread_join(fix_thread, NULL);
}

/**
 * Starts our multi-threaded server.
 * @param arg Options structure parsed from command line arguments
 * @return NULL
 */
void* startServer(void * arg)
{
	g_ns = new NodeServiceHandler();
	//Set up node ID and port
	nodeOptions * op = (nodeOptions*)arg;
	NodeInfo info;
	//Setup the node info
	info.id = op->id;
	info.port = op->port;
	g_ns->setNodeInfo(info);
	//set the introducer
	info.id = 0;
	info.port = op->introducerPort;
	g_ns->setIntroducer(info);
	//if this is the introducer, we can setup the finger table
	if(op->id == 0)
	{
		g_ns->setChordInfo(op->m);
	}
	//Start the server
	shared_ptr<NodeServiceHandler> handler(g_ns);
	shared_ptr<TProcessor> processor(new NodeServiceProcessor(handler));
	shared_ptr<TServerTransport> serverTransport(new TServerSocket(op->port));
	shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
	shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
	//threaded server to serve multiple requests
	TThreadedServer server(processor, serverTransport, transportFactory, protocolFactory);
	server_setup = true;
	server.serve();
	return NULL;

}

/**
 * A separate thread to call our stabilization algorithm
 * on the intervals requested by the user.
 * @param arg NULL
 * @return NULL
 */
void* stabilizeThread(void * arg)
{
	if(gi_stabilize_time <= 0)
	{
		gi_stabilize_time = 3;
	}
	//just to make sure
	while(!server_setup);
	sleep(3);
	for(;;)
	{
		sleep(gi_stabilize_time);
		if(g_ns != NULL)
		{
			g_ns->stabilize();
		}
	}
	return NULL;
}

/**
 * A separate thread to call our fix algorithm
 * on the intervals requested by the user.
 * @param arg NULL
 * @return NULL
 */
void* fixThread(void * arg)
{
	if(gi_fix_time <= 0)
	{
		gi_fix_time = 3;
	}
	//just to make sure
	while(!server_setup);
	sleep(3);
	for(;;)
	{
		sleep(gi_fix_time);
		if(g_ns != NULL)
		{
			g_ns->updateFingerTable();
		}
	}
	return NULL;
}